iT邦幫忙

2022 iThome 鐵人賽

DAY 16
0
Mobile Development

Flutter 30: from start to store系列 第 16

Flutter介紹:頁面的建構 - Custom Widget

  • 分享至 

  • xImage
  •  

將一些常用的widget組合提取出來,可以增加組件的複用性,在更動時也比較好維護。

這同時也考驗這個自定義的組件是否充分設計:有新的需求的時候,應該要可以直接擴充組件,而非對組件進行修改。

今天我們會以APOD專案中最常用到的「天文圖文描述組件」作為範例,將之提取出來建立成自定義的組件,在多個頁面重複使用。

好的,那我們就開始吧!


抽取組件

  • 定義目前需要抽出來的widget,也就是含有天文圖片、描述和筆記框的頁面,我們稱之為AstroPicture
  • 包括了標題、圖片、收藏按鈕、圖片描述、筆記欄 等五個區域的組件。

定義組件 prop

  • 考慮到AstroPicture可能會有三種使用情況:
    • 在主頁面(MainPage)顯示每日的圖片
    • 在收藏頁面(FavoritePage)點擊標題列表時,導向對應標題的天文圖片頁面
    • 在月曆頁面(CalendarPage)點擊對應日期時,導向對應日期的天文圖片頁面
  • 每次導向AstroPicture 時,要提供以下對應資訊:
    • 標題(title)
    • 天文圖片連結(pictureUrl)
    • 圖片描述(desc): discription的縮寫
    • 使用者自己的筆記(note)
    • 喜好狀態(isFavorite)
  1. lib下創建widget資料夾,創建新檔案AstroPicture

  2. AstroPicture 設定好以下prop

    class AstroPicture extends StatefulWidget {
      final String title; // 標題
      final String pictureUrl; // 圖片來源
      final String desc; // 圖片描述
      final String note; // 手札
      final bool isFavorite; // 是否收藏 
    
      const AstroPicture(
          {super.key,
          required this.title,
          required this.pictureUrl,
          required this.desc,
          this.note = '', // 非必要,若沒有手札紀錄預設為空字串
          this.isFavorite = false // 非必要,預設為非收藏的圖片});
    
      @override
      State<AstroPicture> createState() => _AstroPictureState();
    }
    

取用widget prop

  • 將原本屬於這個頁面的UI從MainPage拉過來,並將標題、圖片等會由外部傳入的資料改用 widget.PROP_NAME顯示
    // 定義頁面筆記模式
    enum NoteType {
      text,
      editable,
    }
    
    class AstroPicture extends StatefulWidget {
      final String title;
      final String pictureUrl;
      final String desc;
      final String note;
      final bool isFavorite;
    
      const AstroPicture(
          {super.key,
          required this.title,
          required this.pictureUrl,
          required this.desc,
          this.note = '',
          this.isFavorite = false});
    
      @override
      State<AstroPicture> createState() => _AstroPictureState();
    }
    
    class _AstroPictureState extends State<AstroPicture> {
      final TextEditingController _controller = TextEditingController();
      NoteType _noteType = NoteType.editable;
    
      @override
      void initState() {
        super.initState();
        _controller.text = widget.note;
      }
    
      @override
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        Size deviceScreen = MediaQuery.of(context).size;
    
        return SingleChildScrollView(
          child: Column(children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(10.0),
              child: Text(
                widget.title,
                style: const TextStyle(fontSize: 30, fontWeight: FontWeight.w500),
              ),
            ),
            Stack(
              children: [
                SizedBox(
                  width: deviceScreen.width,
                  child: widget.pictureUrl.isNotEmpty
                      ? Image.network(widget.pictureUrl, frameBuilder:
                          (context, child, frame, wasSynchronouslyLoaded) {
                          if (wasSynchronouslyLoaded) {
                            return child;
                          }
                          return AnimatedOpacity(
                            opacity: frame == null ? 0 : 1,
                            duration: const Duration(seconds: 1),
                            curve: Curves.easeOut,
                            child: child,
                          );
                        })
                      : SizedBox(
                          width: deviceScreen.width,
                          height: deviceScreen.width,
                          child: const Center(
                              child: Text(
                            '圖片載入錯誤',
                            style: TextStyle(color: Colors.red, fontSize: 30),
                          )),
                        ),
                ),
                widget.pictureUrl.isNotEmpty
                    ? Positioned(
                        top: 10.0,
                        right: 10.0,
                        child: ElevatedButton(
                            style: ElevatedButton.styleFrom(
                                backgroundColor: Colors.white24),
                            onPressed: () {
                              print('add to favorite');
                            },
                            child: widget.isFavorite
                                ? Icon(
                                    Icons.favorite,
                                    color: Colors.pink[200],
                                  )
                                : const Icon(
                                    Icons.favorite_border_outlined,
                                    color: Colors.white,
                                  )),
                      )
                    : Container(),
              ],
            ),
            Padding(
              padding: const EdgeInsets.all(10.0),
              child: Text(
                widget.desc,
                style: const TextStyle(fontSize: 16, color: Colors.blueGrey),
              ),
            ),
            _noteType == NoteType.text
                ? Row(
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                      Expanded(
                          child: Container(
                              padding: const EdgeInsets.all(10.0),
                              child: Text(_controller.text))),
                      Container(
                          width: 100,
                          padding: const EdgeInsets.all(10),
                          child: OutlinedButton(
                              style: OutlinedButton.styleFrom(
                                fixedSize: const Size(50, 50),
                              ),
                              onPressed: () {
                                setState(() {
                                  _noteType = NoteType.editable;
                                });
                              },
                              child: const Icon(Icons.edit)))
                    ],
                  )
                : Row(
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                      Expanded(
                        child: Padding(
                          padding: const EdgeInsets.all(10.0),
                          child: ConstrainedBox(
                            constraints: const BoxConstraints(minWidth: 100),
                            child: TextField(
                              controller: _controller,
                              decoration: const InputDecoration(
                                filled: true,
                                fillColor: Colors.white,
                                contentPadding: EdgeInsets.all(10),
                                border: InputBorder.none,
                              ),
                              maxLines: 5,
                              minLines: 3,
                            ),
                          ),
                        ),
                      ),
                      Container(
                        width: 100,
                        padding: const EdgeInsets.all(10),
                        child: OutlinedButton(
                          style: OutlinedButton.styleFrom(
                            fixedSize: const Size(50, 50),
                          ),
                          onPressed: () {
                            setState(() {
                              _noteType = NoteType.text;
                            });
                          },
                          child: const Icon(
                            Icons.save,
                          ),
                        ),
                      )
                    ],
                  ),
          ]),
        );
      }
    }
    

MainPage加入AstroPicture widget

  • MainPage原本放圖片的位置置入AstroPicture, 並給予對應的參數:
    if (snapshot.hasData) {
          ApodData? data = snapshot.data;
          return AstroPicture(
            title: data?.title ?? '',
            pictureUrl: data?.url ?? '',
            desc: data?.desc ?? '',
            note: 'Place your note here!', // 待日後將儲存的筆記放進來
            isFavorite: false, // 待日後將收藏狀態放進來
          );
        }
    
    可以看到AstroPicture被提取出來,之後任何頁面都可以一起使用這個Widget,只要給予不一樣的參數就可以顯示不同的內容。
    https://ithelp.ithome.com.tw/upload/images/20221003/20152234Ciabrq0KxH.png

FavoritePage加入AstroPicture

  • ListView設定為點擊項目之後,會導頁到AstroPicture
    class FavoritePage extends StatefulWidget {
      const FavoritePage({super.key});
    
      @override
      State<FavoritePage> createState() => _FavoritePageState();
    }
    
    class _FavoritePageState extends State<FavoritePage> {
      @override
      Widget build(BuildContext context) {
        return Center(
          child: ListView.builder(
            itemBuilder: (BuildContext context, int index) {
              return Container(
                height: 100,
                padding: const EdgeInsets.all(5.0),
                child: SizedBox(
                  height: 100,
                  child: InkWell(
                    onTap: () {
                      Navigator.push(context, MaterialPageRoute(builder: (context) {
                        return Scaffold(
                            appBar: AppBar(
                              title: const Text('one of the title'),
                            ),
                            body: const AstroPicture(
                              title: 'title of astro picture',
                              pictureUrl: '',
                              desc: 'desc',
                              note: "don't know what to fill yet",
                              isFavorite: false,
                            )); // 要填什麼資料進去呢??
                      }));
                    },
                    child: const Card(
                        elevation: 5.0,
                        child: Center(
                          child: Text(
                            'I am Image Title',
                            style: TextStyle(
                                fontSize: 20, fontWeight: FontWeight.w500),
                          ),
                        )),
                  ),
                ),
              );
            },
            itemCount: 2,
          ),
        );
      }
    }
    
  • 由於我們在收藏頁面還沒收到被收藏的圖片的相關資料,目前沒辦法正確顯示圖片
    https://ithelp.ithome.com.tw/upload/images/20221003/20152234ygI7HnVfF6.png
  • 本次改動的相關程式碼放在我的github,見Day16相關commit

Recap

今天先嘗試抽出AstroPicture組件,將其放在MainPage, 以及作為FavoritePage點擊列表選項後的導頁目標。

可是,我們要怎麼在FavoritePage得知我在MainPage收藏了哪些頁面?導頁之後頁面上的資料又哪裡來呢?

明天一起來看看App狀態管理,將MainPage收藏的天文資料存到APP層級,讓其他頁面也可以取得,就可以解決頁面間的資料溝通問題囉!


上一篇
Flutter頁面的建構:Icon, TextInput, ListView
下一篇
Flutter介紹:App狀態管理 - app state management
系列文
Flutter 30: from start to store30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言